namespace SimpleWebServices.Data
{
    using System;
    using System.Globalization;
    using System.IO;
    using System.Net;
    using System.Web;
    using System.Xml;
    using System.Xml.Schema;
    using System.Xml.Serialization;
    using System.Xml.XPath;
    using SimpleWebServices.Web;
    using SimpleWebServices.Web.UI.Html;

    [XmlRoot("object")]
    public sealed class XmlDataObject : ValueObject<XmlDataObject>, IXmlSerializable, IResource
    {
        public XmlDataObject()
            : this(null as IRecord)
        {
        }

        public XmlDataObject(IRecord obj)
        {
            this.RegisterProperty(x => x.Cacheability);
            this.RegisterProperty(x => x.Created);
            this.RegisterProperty(x => x.Expires);
            this.RegisterProperty(x => x.Modified);
            this.RegisterProperty(x => x.Status);
            this.RegisterProperty(x => x.Token);
            this.RegisterProperty(x => x.Urn);

            this.RegisterProperty(x => x.ObjectType);
            this.RegisterProperty(x => x.XmlString);

            if (null != obj)
            {
                this.Cacheability = obj.Cacheability;
                this.Created = obj.Created;
                this.Expires = obj.Expires;
                this.Modified = obj.Modified;
                this.Status = obj.Status;
                this.Token = obj.Token;
                this.Urn = obj.Urn;

                this.ObjectType = obj.GetType();
                this.Xml = obj.Serialize();
            }
        }

        public HttpCacheability? Cacheability
        {
            get;
            set;
        }

        public DateTime? Created
        {
            get;
            set;
        }

        public DataCollection Data
        {
            get;
            set;
        }

        public string Description
        {
            get;
            set;
        }

        public string Entity
        {
            get
            {
                return string.Empty;
            }
        }

        public string Etag
        {
            get;
            set;
        }

        public Expiration Expires
        {
            get;
            set;
        }

        public LinkCollection Links
        {
            get;
            set;
        }

        public MetaCollection Metadata
        {
            get;
            set;
        }

        public DateTime? Modified
        {
            get;
            set;
        }

        public HttpStatusCode? Status
        {
            get;
            set;
        }

        public string Title
        {
            get;
            set;
        }

        public Token? Token
        {
            get;
            set;
        }

        public AbsoluteUri Urn
        {
            get;
            set;
        }

        public Type ObjectType
        {
            get;
            set;
        }

        public IXPathNavigable Xml
        {
            get;
            set;
        }

        private string XmlString
        {
            get
            {
                string value = null;

                if (null != this.Xml)
                {
                    value = this.Xml.CreateNavigator().OuterXml;
                }

                return value;
            }
        }

        public IRecord ToRecord()
        {
            IRecord result = ToRecord(this.ObjectType, this.Xml);

            result.Cacheability = this.Cacheability;
            result.Created = this.Created;
            result.Expires = this.Expires;
            result.Modified = this.Modified;
            result.Status = this.Status;
            result.Token = this.Token;
            result.Urn = this.Urn;

            return result;
        }

        XmlSchema IXmlSerializable.GetSchema()
        {
            throw new NotSupportedException();
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            if (null == reader)
            {
                throw new ArgumentNullException("reader");
            }

            string value = reader.GetAttribute("cacheability");
            if (!string.IsNullOrEmpty(value))
            {
                this.Cacheability = (HttpCacheability)Enum.Parse(typeof(HttpCacheability), value);
            }

            value = reader.GetAttribute("created");
            if (!string.IsNullOrEmpty(value))
            {
                this.Created = XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.Utc);
            }

            value = reader.GetAttribute("expires");
            if (!string.IsNullOrEmpty(value))
            {
                this.Expires = value;
            }

            value = reader.GetAttribute("modified");
            if (!string.IsNullOrEmpty(value))
            {
                this.Modified = XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.Utc);
            }

            value = reader.GetAttribute("status");
            if (!string.IsNullOrEmpty(value))
            {
                this.Status = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), value);
            }

            value = reader.GetAttribute("token");
            if (!string.IsNullOrEmpty(value))
            {
                this.Token = value;
            }

            value = reader.GetAttribute("type");
            if (null != value)
            {
                this.ObjectType = Type.GetType(value, true);
            }

            value = reader.GetAttribute("urn");
            if (!string.IsNullOrEmpty(value))
            {
                this.Urn = value;
            }

            if (reader.IsEmptyElement)
            {
                reader.Read();
            }
            else
            {
                string name = reader.Name;
                reader.Read();
                while (true)
                {
                    if (XmlNodeType.None == reader.NodeType)
                    {
                        break;
                    }
                    else if (XmlNodeType.EndElement == reader.NodeType && reader.Name == name)
                    {
                        reader.Read();
                        break;
                    }
                    else if (null != this.Xml)
                    {
                        throw new XmlException("The object element can only have one child element.");
                    }
                    else
                    {
                        XmlDocument document = new XmlDocument();
                        document.LoadXml(reader.ReadOuterXml());
                        this.Xml = document;
                    }
                }
            }
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            if (null == writer)
            {
                throw new ArgumentNullException("writer");
            }

            if (this.Cacheability.HasValue)
            {
                writer.WriteAttributeString("cacheability", XmlConvert.ToString((int)this.Cacheability.Value));
            }

            if (this.Created.HasValue)
            {
                writer.WriteAttributeString("created", XmlConvert.ToString(this.Created.Value, XmlDateTimeSerializationMode.Utc));
            }

            if (null != this.Expires)
            {
                writer.WriteAttributeString("expires", this.Expires);
            }

            if (this.Modified.HasValue)
            {
                writer.WriteAttributeString("modified", XmlConvert.ToString(this.Modified.Value, XmlDateTimeSerializationMode.Utc));
            }

            if (this.Status.HasValue)
            {
                writer.WriteAttributeString("status", XmlConvert.ToString((int)this.Status.Value));
            }

            if (this.Token.HasValue)
            {
                writer.WriteAttributeString("token", this.Token.Value);
            }

            if (null != this.Urn)
            {
                writer.WriteAttributeString("urn", this.Urn);
            }

            if (null != this.ObjectType)
            {
                writer.WriteAttributeString("type", string.Format(CultureInfo.InvariantCulture, "{0}, {1}", this.ObjectType.FullName, this.ObjectType.Assembly.GetName().Name));
            }

            if (null != this.Xml)
            {
                writer.WriteRaw(this.XmlString);
            }
        }

        private static IRecord ToRecord(Type type, IXPathNavigable xml)
        {
            if (null == type)
            {
                throw new ArgumentNullException("type");
            }
            else if (!typeof(IRecord).IsAssignableFrom(type))
            {
                throw new ArgumentOutOfRangeException("type");
            }

            IRecord result = null;

            if (null == xml)
            {
                result = Activator.CreateInstance(type) as IRecord;
            }
            else
            {
                try
                {
                    using (MemoryStream stream = new MemoryStream())
                    {
                        using (StreamWriter writer = new StreamWriter(stream))
                        {
                            writer.Write(xml.CreateNavigator().OuterXml);
                            writer.Flush();
                            stream.Position = 0;

                            result = (IRecord)new XmlSerializer(type).Deserialize(stream);
                        }
                    }
                }
                catch (InvalidOperationException exception)
                {
                    string message = string.Concat(
                        exception.Message,
                        Environment.NewLine,
                        "-------------------",
                        Environment.NewLine,
                        xml.CreateNavigator().OuterXml);
                    throw new XmlException(message, exception);
                }
            }

            return result;
        }
    }
}